package com.example.myapplication;

import androidx.annotation.NonNull;
import androidx.lifecycle.MutableLiveData;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringReader;
import java.net.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.logging.Logger;

import jakarta.json.Json;
import jakarta.json.JsonArray;
import jakarta.json.JsonObject;
import jakarta.json.JsonValue;
import jakarta.json.bind.Jsonb;
import jakarta.json.bind.JsonbBuilder;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
import pojo.PLD_daemon;
import pojo.Timed;

public class Scanner {

    Inet4Address network;
    int udpPort = 2021;
    int wsPort = 3000;
    Consumer<String> logger;

    Map<Inet4Address, ScanEntry> foundEntries = new ConcurrentHashMap<>();

    public Scanner(Inet4Address network) {
        this.network = network;
    }

    public void setLogger(Consumer<String> logger) {
        this.logger = logger;
    }

    public Map<Inet4Address, ScanEntry> getFoundEntries() {
        return foundEntries;
    }

    public void scan() {
        if (foundEntries.isEmpty()) {
            String hostAddress = network.getHostAddress();
            String main = hostAddress.substring(0, hostAddress.lastIndexOf(".") + 1);
            ExecutorService service = Executors.newCachedThreadPool();
            List<Future<InetAddress>> ipAvailability = new ArrayList<>();
            for (int i = 1; i < 254; i++) {
                final String address = main + i;
                ipAvailability.add(service.submit(() -> {
                    InetAddress ip = InetAddress.getByName(address);
                    if (!network.equals(ip) && ip.isReachable(1000)) {
                        return ip;
                    }
                    return null;
                }));
            }

            ipAvailability.stream().map(f -> { try{ return f.get(); } catch (Exception e) { return null; }})
                    .filter(Objects::nonNull).forEach(ip -> {
                        logger.accept(ip.getHostAddress());
                        try {
                                PLD_daemon daemon = discovery((Inet4Address) ip, udpPort, network);
                                if (daemon != null) {
                                    foundEntries.put((Inet4Address) ip, new ScanEntry((Inet4Address) ip, daemon, list()));
                                } else {
                                    logger.accept("null daemon for " + ip);
                                }
                        } catch (Exception e) {
                            logger.accept(ip + " " + e.getMessage());
                        }
            });

            service.shutdown();
            try {
                service.awaitTermination(10, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                Logger.getLogger("main").throwing("scanner", "scan", e);
            }
        }
    }

    public synchronized Inet4Address getReacthome() {
        return foundEntries.values().stream().filter(e -> e.getList() != null && !e.getList().isEmpty()).findFirst().map(ScanEntry::getAddress).orElse(null);
    }

    public PLD_daemon discovery() {
        return discovery(getReacthome(), udpPort, network);
    }

    static public PLD_daemon discovery(Inet4Address reacthome, int udpPort, Inet4Address network) {
        if (reacthome == null) {
            return null;
        }
        try (DatagramSocket send = new DatagramSocket(udpPort, network)) {
            send.setSoTimeout(500);
            byte[] buf = Json.createObjectBuilder().add("type", "discovery").build().toString().getBytes();
            DatagramPacket packet = new DatagramPacket(buf, buf.length, reacthome, udpPort);
            send.send(packet);
            byte[] data = new byte[20048];
            DatagramPacket remote = new DatagramPacket(data, data.length);
            send.receive(remote);
            JsonObject discovery = Json.createReader(new ByteArrayInputStream(data)).readObject();

            if (discovery.containsKey("payload")) {
                Jsonb jsonb = JsonbBuilder.create();
                return jsonb.fromJson(discovery.get("payload").toString(), PLD_daemon.class);
            }

            PLD_daemon d = new PLD_daemon();
            d.setTitle(discovery.toString() + " " + reacthome);
            return d;

        } catch (IOException e) {
            Logger.getLogger("main").info("" + reacthome + " " + e.getMessage());
            throw new RuntimeException(e);
        }

    }

    public Map<String, Timed> list() {
        try {
            Request request = new Request.Builder().url("ws://" + Optional.ofNullable(getReacthome()).map(Inet4Address::getHostAddress).orElseThrow() + ":" + wsPort).build();
            TypedActionsListener listener = new TypedActionsListener();
            new OkHttpClient().newWebSocket(request, listener)
                    .send(
                            Json.createObjectBuilder().add("type", "list").build().toString()
                    );
            return listener.getPayloads();
        } catch (Exception e) {
            return Collections.emptyMap();
        }
    }
    static JsonValue jsonString = Json.createValue("");

    static class TypedActionsListener extends WebSocketListener {
        Map<String, Timed> payloads = new HashMap<>();
        AtomicInteger remoteSize = new AtomicInteger(0);

        @Override
        public void onMessage(@NonNull WebSocket webSocket, @NonNull String text) {
            JsonObject object = Json.createReader(new StringReader(text)).readObject();
            if (object.containsKey("type")
                    && "list".equals(object.getString("type"))
            ) {
                try {
                    JsonArray state = object.getJsonArray("state");
                    remoteSize.set(state.size());
                    JsonObject getRequest = Json.createObjectBuilder()
                            .add("type", "get")
                            .add("state", state)
                            .build();
                    webSocket.send(getRequest.toString());
                } catch (Exception ignore) {

                }
            } else if (object.containsKey("type")
                    && "ACTION_SET".equals(object.getString("type"))
                    && object.containsKey("payload")
                    && object.getJsonObject("payload").containsKey("type")
            ) {
                JsonObject payload = object.getJsonObject("payload");
                String id = object.getString("id");
                String type = payload.computeIfAbsent("type", s -> jsonString).toString().replaceAll("\"", "");
                Class<?> ref = null;
                try {
                    ref = Class.forName("pojo.PLD_" + type);
                    if (ref.isInstance(Timed.class)) {
                        Jsonb jsonb = JsonbBuilder.create();
                        Timed entry = (Timed) jsonb.fromJson(payload.toString(), ref);
                        entry.setId(id);
                        payloads.put(id, entry);
                        jsonb.close();
                    }
                } catch (Exception e) {
                    remoteSize.decrementAndGet();
                }

            }
        }

        public Map<String, Timed> getPayloads() {
            try {
                while (payloads.size() < remoteSize.get()) {
                    Thread.sleep(200);
                }
            } catch (InterruptedException ignore) {

            }
            return payloads;
        }
    }
}
